﻿using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;

namespace BfLib.Imaging
{
    public enum ImageType
    {
        Jpeg = 0,
        Bitmap = 1,
        Gif = 2,
        Tiff = 3,
        Png = 4
    }

    public enum HorizontalEdge
    {
        FromRight = 0,
        FromLeft = 1
    }

    public enum VerticalEdge
    {
        FromTop = 0,
        FromBottom = 1
    }

    public struct StampPosition
    {
        public int HPos;
        public HorizontalEdge HEdge;
        public int VPos;
        public VerticalEdge VEdge;
    }

    public class ImageOperator : IDisposable
    {
        #region Private member variables

        private Bitmap _baseImage;
        private Bitmap _StampImage;
        private Point _StampPosition;
        private int _radius = 0;
        private Color _bgColor = System.Drawing.Color.Black;
        private float _dpiX = 0.0F;
        private float _dpiY = 0.0F;
        private byte _opacity = 127;

        #endregion

        #region Constructors

        public ImageOperator(string BaseImage)
        {
            _baseImage = new Bitmap(BaseImage);
            _dpiX = _baseImage.HorizontalResolution;
            _dpiY = _baseImage.VerticalResolution;
        }

        public ImageOperator(byte[] BaseImage)
        {
            _baseImage = ByteArrayToBitmap(BaseImage);
            _dpiX = _baseImage.HorizontalResolution;
            _dpiY = _baseImage.VerticalResolution;
        }

        public ImageOperator(Bitmap BaseImage)
        {
            _baseImage = BaseImage;
            _dpiX = _baseImage.HorizontalResolution;
            _dpiY = _baseImage.VerticalResolution;
        }

        public ImageOperator(Stream BaseImage)
        {
            _baseImage = new Bitmap(BaseImage);
            _dpiX = _baseImage.HorizontalResolution;
            _dpiY = _baseImage.VerticalResolution;
        }

        public void Dispose()
        {
            if (_baseImage != null)
            {
                _baseImage.Dispose();
            }
            if (_StampImage != null)
            {
                _StampImage.Dispose();
            }
        }
     
        #endregion

        #region Other Public Methods

        public void SetPixelFormat(PixelFormat PFormat)
        {
            using (Bitmap clone = new Bitmap(this._baseImage.Width, this._baseImage.Height, PFormat))
            {
                using (Graphics gr = Graphics.FromImage(clone))
                {
                    gr.DrawImage(this._baseImage, new Rectangle(0, 0, clone.Width, clone.Height));
                }

                this._baseImage.Dispose();
                this._baseImage = new Bitmap(clone);
            }
        }

        public void RoundCorners(int Radius, Color BackgroundColor)
        {
            int Diameter = Radius * 2;
            using (Bitmap RoundedImage = new Bitmap(this._baseImage.Width, this._baseImage.Height))
            {
                RoundedImage.SetResolution(this._baseImage.HorizontalResolution, this._baseImage.VerticalResolution);

                using (Graphics g = Graphics.FromImage(RoundedImage))
                {
                    g.Clear(BackgroundColor);
                    //g.SmoothingMode = SmoothingMode.HighQuality;

                    Brush brush = new TextureBrush(this._baseImage);
                    using (GraphicsPath gp = new GraphicsPath())
                    {
                        gp.AddArc(0, 0, Diameter, Diameter, 180, 90);
                        gp.AddArc(0 + RoundedImage.Width - Diameter, 0, Diameter, Diameter, 270, 90);
                        gp.AddArc(0 + RoundedImage.Width - Diameter, 0 + RoundedImage.Height - Diameter, Diameter, Diameter, 0, 90);
                        gp.AddArc(0, 0 + RoundedImage.Height - Diameter, Diameter, Diameter, 90, 90);
                        g.FillPath(brush, gp);
                    }
                    this._baseImage.Dispose();
                    this._baseImage = new Bitmap(RoundedImage);
                }
            }
        }

        public bool OptimizeJPEG(string Outfile, int Quality = 80)
        {
            if ((Quality < 1) || (Quality > 100))
            {
                //throw new Exception("Quality must be 0>q<=100");
                return false;
            }

            try
            {
                //create an encoder parameter for the image quality
                EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, Quality);
                //get the jpeg codec
                ImageCodecInfo codec = GetEncoderInfo("image/jpeg");

                if (codec == null)
                {
                    throw new Exception("No image encoder found for JPEG (image/jpeg)");
                }

                EncoderParameters encoderParams = new EncoderParameters(1);
                encoderParams.Param[0] = qualityParam;
                this._baseImage.Save(Outfile, codec, encoderParams);

                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool Save(Bitmap BmpOut, string FileName, ImageType ImageType, long Quality)
        {
            if ((Quality < 1) || (Quality > 100))
            {
                throw new Exception("Quality must be 0>q<=100");
            }

            string ImageMimeType = GetMimeType(ImageType);

            try
            {
                //create an encoder parameter for the image quality
                EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, Quality);

                //get the jpeg codec
                ImageCodecInfo codec = GetEncoderInfo(ImageMimeType);

                if (codec == null)
                {
                    throw new Exception(String.Format("No image encoder found for mime type '{0}'", ImageMimeType));
                }

                EncoderParameters encoderParams = new EncoderParameters(1);
                encoderParams.Param[0] = qualityParam;
                BmpOut.Save(FileName, codec, encoderParams);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool Save(Bitmap BmpOut, ref Stream stream, ImageType ImageType, long Quality)
        {
            if ((Quality < 1) || (Quality > 100))
            {
                throw new Exception("Quality must be 0>q<=100");
            }

            string ImageMimeType = GetMimeType(ImageType);

            try
            {
                //create an encoder parameter for the image quality
                EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, Quality);
                //get the jpeg codec
                ImageCodecInfo codec = GetEncoderInfo(ImageMimeType);

                if (codec == null)
                {
                    throw new Exception(String.Format("No image encoder found for mime type '{0}'", ImageMimeType));
                }

                EncoderParameters encoderParams = new EncoderParameters(1);
                encoderParams.Param[0] = qualityParam;
                BmpOut.Save(stream, codec, encoderParams);
                return true;
            }
            catch
            {
                return false;
            }
        }

        #region Public OverlayImage methods

        //Methods to overlay an image on top of another,  with an Opacity parameter (0-255) 
        //and parameters to round the corners of the Stamp image and set the background
        //color of the cut out corners.
        public bool OverlayImage(string StampImage, StampPosition Position, Color? BackgroundColor = null, int StampRoundCornerRadius = 0, byte Opacity = 255)
        {
            if (!BackgroundColor.HasValue)
            {
                BackgroundColor = Color.Transparent;
            }

            if (_StampImage != null)
            {
                _StampImage.Dispose();
            }

            _StampImage = new Bitmap(StampImage);
            _StampImage.SetResolution(_dpiX, _dpiY);

            SetWatermarkPoint(Position);

            _radius = StampRoundCornerRadius;
            _bgColor = BackgroundColor.Value;
            _opacity = Opacity;
            this.ProcessWatermark();

            _StampImage.Dispose();

            return true;
        }

        public bool OverlayImage(byte[] StampImage, StampPosition Position, Color? BackgroundColor = null, int StampRoundCornerRadius = 0, byte Opacity = 255)
        {
            if (!BackgroundColor.HasValue)
            {
                BackgroundColor = Color.Transparent;
            }

            if (_StampImage != null)
            {
                _StampImage.Dispose();
            }

            _StampImage = ByteArrayToBitmap(StampImage);
            _StampImage.SetResolution(_dpiX, _dpiY);

            SetWatermarkPoint(Position);

            _radius = StampRoundCornerRadius;
            _bgColor = BackgroundColor.Value;
            _opacity = Opacity;

            this.ProcessWatermark();

            _StampImage.Dispose();

            return true;
        }

        public bool OverlayImage(Bitmap StampImage, StampPosition Position, Color? BackgroundColor = null, int StampRoundCornerRadius = 0, byte Opacity = 255)
        {
            if (!BackgroundColor.HasValue)
            {
                BackgroundColor = Color.Transparent;
            }

            if (_StampImage != null)
            {
                _StampImage.Dispose();
            }

            _StampImage = StampImage;
            _StampImage.SetResolution(_dpiX, _dpiY);

            SetWatermarkPoint(Position);


            _radius = StampRoundCornerRadius;
            _bgColor = BackgroundColor.Value;
            _opacity = Opacity;

            this.ProcessWatermark();

            _StampImage.Dispose();

            return true;
        }

        public bool OverlayImage(Stream StampImage, StampPosition Position, Color? BackgroundColor = null, int StampRoundCornerRadius = 0, byte Opacity = 255)
        {
            if (!BackgroundColor.HasValue)
            {
                BackgroundColor = Color.Transparent;
            }

            if (_StampImage != null)
            {
                _StampImage.Dispose();
            }
            _StampImage = new Bitmap(StampImage);
            _StampImage.SetResolution(_dpiX, _dpiY);

            SetWatermarkPoint(Position);

            _radius = StampRoundCornerRadius;
            _bgColor = BackgroundColor.Value;
            _opacity = Opacity;
            this.ProcessWatermark();
            _StampImage.Dispose();
            return true;
        }

        private void SetWatermarkPoint(StampPosition Position)
        {
            if (Position.HEdge == HorizontalEdge.FromLeft)
            {
                _StampPosition.X = Position.HPos;
            }
            else
            {
                _StampPosition.X = this._baseImage.Width - Position.HPos - _StampImage.Width;
            }

            if (Position.VEdge == VerticalEdge.FromTop)
            {
                _StampPosition.Y = Position.VPos;
            }
            else
            {
                _StampPosition.Y = this._baseImage.Height - Position.VPos - _StampImage.Height;
            }
        }

        #endregion

        #region Public resizing methods

        public void ResizeImage(int NewWidth, int NewHeight, bool HighQuality)
        {
            using (Bitmap newImage = new Bitmap(NewWidth, NewHeight))
            {
                using (Graphics gr = Graphics.FromImage(newImage))
                {
                    if (HighQuality)
                    {
                        gr.SmoothingMode = SmoothingMode.HighQuality;
                        gr.InterpolationMode = InterpolationMode.HighQualityBicubic;
                        gr.PixelOffsetMode = PixelOffsetMode.HighQuality;
                    }
                    else
                    {
                        gr.SmoothingMode = SmoothingMode.AntiAlias;
                        gr.InterpolationMode = InterpolationMode.Default;
                        gr.PixelOffsetMode = PixelOffsetMode.Default;
                    }
                    gr.DrawImage(_baseImage, new Rectangle(0, 0, NewWidth, NewHeight));
                }

                this._baseImage.Dispose();
                this._baseImage = new Bitmap(newImage);
            }
        }

        public void EnsureMaximumWidth(int MaxWidth, bool HighQuality)
        {
            double scale = Convert.ToDouble(MaxWidth) / Convert.ToDouble(this._baseImage.Width);
            int NewHeight = Convert.ToInt32(Convert.ToDouble(this._baseImage.Height) * scale);
            ResizeImage(MaxWidth, NewHeight, HighQuality);
        }

        public void EnsureMaximumHeight(int MaxHeight, bool HighQuality)
        {
            double scale = Convert.ToDouble(MaxHeight) / Convert.ToDouble(this._baseImage.Height);
            int NewWidth = Convert.ToInt32(Convert.ToDouble(this._baseImage.Width) * scale);
            ResizeImage(NewWidth, MaxHeight, HighQuality);
        }

        public void FitInBox(int BoxWidth, int BoxHeight, bool HighQuality)
        {
            double imgAspect = Convert.ToDouble(this._baseImage.Width) / Convert.ToDouble(this._baseImage.Height);
            double boxAspect = Convert.ToDouble(BoxWidth) / Convert.ToDouble(BoxHeight);
            double scale = 0.0;
            int NewWidth = 0;
            int NewHeight = 0;

            if (imgAspect > boxAspect)
            {
                //will be horizontal in the box, (blank control space above and below)
                NewWidth = BoxWidth;
                scale = Convert.ToDouble(NewWidth) / Convert.ToDouble(this._baseImage.Width);
                NewHeight = Convert.ToInt32(Convert.ToDouble(this._baseImage.Height) * scale);
            }
            else
            {
                //will be vertical in the box, (blank control space left and right)
                NewHeight = BoxHeight;
                scale = Convert.ToDouble(NewHeight) / Convert.ToDouble(this._baseImage.Height);
                NewWidth = Convert.ToInt32(Convert.ToDouble(this._baseImage.Width) * scale);
            }

            ResizeImage(NewWidth, NewHeight, HighQuality);
        }

        public void ScaleImageArea(double Scale, bool HighQuality)
        {
            double scale = Math.Sqrt(Scale);
            int NewWidth = Convert.ToInt32(Convert.ToDouble(this._baseImage.Width) * scale);
            int NewHeight = Convert.ToInt32(Convert.ToDouble(this._baseImage.Height) * scale);
            ResizeImage(NewWidth, NewHeight, HighQuality);
        }

        public void ScaleImageByDimension(double Scale, bool HighQuality)
        {
            int NewWidth = Convert.ToInt32(Convert.ToDouble(this._baseImage.Width) * Scale);
            int NewHeight = Convert.ToInt32(Convert.ToDouble(this._baseImage.Height) * Scale);
            ResizeImage(NewWidth, NewHeight, HighQuality);
        }

        public void FitInBoxWithBG(int BoxWidth, int BoxHeight, bool HighQuality, Color BackgroundColor)
        {
            //double imgAspect = Convert.ToDouble(this._baseImage.Width) / Convert.ToDouble(this._baseImage.Height);
            //double boxAspect = Convert.ToDouble(BoxWidth) / Convert.ToDouble(BoxHeight);
            //double scale = 0.0;
            //int NewWidth = 0;
            //int NewHeight = 0;
            //if (imgAspect > boxAspect)
            //{
            //    //will be horizontal in the box, (blank control space above and below)
            //    NewWidth = BoxWidth;
            //    scale = Convert.ToDouble(NewWidth) / Convert.ToDouble(this._baseImage.Width);
            //    NewHeight = Convert.ToInt32(Convert.ToDouble(this._baseImage.Height) * scale);
            //}
            //else
            //{
            //    //will be vertical in the box, (blank control space left and right)
            //    NewHeight = BoxHeight;
            //    scale = Convert.ToDouble(NewHeight) / Convert.ToDouble(this._baseImage.Height);
            //    NewWidth = Convert.ToInt32(Convert.ToDouble(this._baseImage.Width) * scale);
            //}
            //ResizeImage(NewWidth, NewHeight, HighQuality);

            FitInBox(BoxWidth, BoxHeight, HighQuality);


            if ((BoxWidth != _baseImage.Width) || (BoxHeight != _baseImage.Height))
            {
                using (Bitmap bgimg = new Bitmap(BoxWidth, BoxHeight, PixelFormat.Format32bppArgb))
                {
                    using (Graphics gr = Graphics.FromImage(bgimg))
                    {
                        SolidBrush brush = new SolidBrush(BackgroundColor);
                        gr.FillRectangle(brush, 0, 0, BoxWidth, BoxHeight);

                        Point pt = new Point();

                        if (BoxHeight > _baseImage.Height)  //there will be head room, foot room
                        {
                            pt.X = 0;
                            pt.Y = (BoxHeight - _baseImage.Height) / 2;
                        }
                        else                                //there will be wide room
                        {
                            pt.X = (BoxWidth - _baseImage.Width) / 2;
                            pt.Y = 0;
                        }

                        gr.DrawImage(_baseImage, new Rectangle(pt.X, pt.Y, _baseImage.Width, _baseImage.Height));
                    }

                    this._baseImage.Dispose();
                    this._baseImage = new Bitmap(bgimg);
                }
            }
        }

        #endregion

        #endregion

        #region Private methods

        private void ProcessWatermark()
        {
            ValidateStampPosition();

            #region Get settings
            //do verification that Stamp image area is smaller than or equal to base image
            int area1 = 0, area2 = 0;
            area1 = _baseImage.Width * _baseImage.Height;
            area2 = _StampImage.Width * _StampImage.Height;
            if (area2 > area1)
            {
                throw new Exception("Stamp image area is larger than base image area.");
            }
            if (_StampPosition.X + _StampImage.Width > _baseImage.Width)
            {
                throw new Exception("StampImage extends past the X limit of the BaseImage.");
            }
            if (_StampPosition.Y + _StampImage.Height > _baseImage.Height)
            {
                throw new Exception("StampImage extends past the Y limit of the BaseImage.");
            }
            #endregion

            #region Do resolution and radius
            //Adjust for disparate DPI values
            if ((_baseImage.HorizontalResolution != _StampImage.HorizontalResolution)
                || (_baseImage.VerticalResolution != _StampImage.VerticalResolution))
            {
                _StampImage.SetResolution(this._dpiX, this._dpiY);
            }

            if (_radius > 0)
            {
                RoundCorners(ref _StampImage, _radius, _bgColor);
            }

            _StampImage.SetResolution(this._dpiX, this._dpiY);
            #endregion

            Graphics canvas = Graphics.FromImage(_baseImage);

            #region Opacity proc and canvas save

            if (_opacity < 255)
            {
                float op = (float)_opacity / 255f;
                ImageAttributes imageAttributes = new ImageAttributes();
                ColorMap colorMap = new ColorMap();
                colorMap.OldColor = _baseImage.GetPixel(0, 0);
                //colorMap.OldColor = Color.FromArgb(255, 0, 255, 0);
                colorMap.NewColor = Color.FromArgb(0, 0, 0, 0);
                ColorMap[] remapTable = { colorMap };
                imageAttributes.SetRemapTable(remapTable, ColorAdjustType.Bitmap);
                float[][] colorMatrixElements = { 
                                               new float[] {1.0f,  0.0f,  0.0f,  0.0f, 0.0f},
                                               new float[] {0.0f,  1.0f,  0.0f,  0.0f, 0.0f},
                                               new float[] {0.0f,  0.0f,  1.0f,  0.0f, 0.0f},
                                               new float[] {0.0f,  0.0f,  0.0f,  op, 0.0f},
                                               new float[] {0.0f,  0.0f,  0.0f,  0.0f, 1.0f}
                                            };
                ColorMatrix wmColorMatrix = new ColorMatrix(colorMatrixElements);
                imageAttributes.SetColorMatrix(wmColorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

                canvas.DrawImage(_StampImage,
                    new Rectangle(_StampPosition.X, _StampPosition.Y, _StampImage.Width, _StampImage.Height),
                    0,
                    0,
                    _StampImage.Width,
                    _StampImage.Height,
                    GraphicsUnit.Pixel,
                    imageAttributes);
            }
            else
            {
                canvas.DrawImage(_StampImage,
                    new Rectangle(_StampPosition.X, _StampPosition.Y, _StampImage.Width, _StampImage.Height),
                    0,
                    0,
                    _StampImage.Width,
                    _StampImage.Height,
                    GraphicsUnit.Pixel);
            }
            #endregion

            canvas.Save();

            //Important to reset to defaults as subsequent ProcessWatermark() calls using same instance will use old values
            ResetVariables();
        }

        private void ResetVariables()
        {
            this._radius = 0;
            this._bgColor = System.Drawing.Color.Black;
            this._dpiX = 96;
            this._dpiY = 96;
            this._opacity = 255;
        }

        private void ValidateStampPosition()
        {
            if ((this._StampPosition.X < 0) || (this._StampPosition.Y < 0) ||
                (this._StampPosition.X + this._StampImage.Width > this._baseImage.Width) ||
                (this._StampPosition.Y + this._StampImage.Height > this._baseImage.Height))
            {
                throw new BfLib.Imaging.Exceptions.ImageOverlayPositionException("Invalid Stamp image poistion. Some or all of the Stamp would be outside of the base image.");
            }
        }

        private void RoundCorners(ref Bitmap Bmp, int Radius, Color BackGroundColor)
        {
            Radius *= 2;
            using (Bitmap RoundedImage = new Bitmap(Bmp.Width, Bmp.Height))
            {
                RoundedImage.SetResolution(Bmp.HorizontalResolution, Bmp.VerticalResolution);
                using (Graphics g = Graphics.FromImage(RoundedImage))
                {
                    g.Clear(BackGroundColor);
                    g.SmoothingMode = SmoothingMode.HighQuality;
                    Brush brush = new TextureBrush(Bmp);

                    using (GraphicsPath gp = new GraphicsPath())
                    {
                        gp.AddArc(0, 0, Radius, Radius, 180, 90);
                        gp.AddArc(0 + RoundedImage.Width - Radius, 0, Radius, Radius, 270, 90);
                        gp.AddArc(0 + RoundedImage.Width - Radius, 0 + RoundedImage.Height - Radius, Radius, Radius, 0, 90);
                        gp.AddArc(0, 0 + RoundedImage.Height - Radius, Radius, Radius, 90, 90);
                        g.FillPath(brush, gp);
                    }

                    Bmp = new Bitmap(RoundedImage);
                }
            }
        }

        private Bitmap ByteArrayToBitmap(byte[] byteArrayIn)
        {
            MemoryStream ms = new MemoryStream(byteArrayIn);
            return new Bitmap(ms);
        }

        private string GetMimeType(ImageType ImgType)
        {
            switch (ImgType)
            {
                case ImageType.Bitmap:
                    {
                        return "image/bitmap";
                    }
                case ImageType.Gif:
                    {
                        return "image/gif";
                    }
                case ImageType.Jpeg:
                    {
                        return "image/jpeg";
                    }
                case ImageType.Png:
                    {
                        return "image/png";
                    }
                default: //case ImageType.Tiff:
                    {
                        return "image/tiff";
                    }
            }
        }

        private ImageCodecInfo GetEncoderInfo(String mimeType)
        {
            int j;
            ImageCodecInfo[] encoders;
            encoders = ImageCodecInfo.GetImageEncoders();
            for (j = 0; j < encoders.Length; ++j)
            {
                if (encoders[j].MimeType == mimeType)
                    return encoders[j];
            }
            return null;
        }

        #endregion

        #region Public accessors

        public Bitmap ResultImage
        {
            get
            {
                return _baseImage;
            }
        }

        public Bitmap StampImage
        {
            get
            {
                return _StampImage;
            }
        }

        #endregion
    }
}
